package com.icoolkj.ms.auth.manager;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.icoolkj.ms.api.auth.bo.UserInfoInTokenBO;
import com.icoolkj.ms.api.auth.vo.TokenInfoVO;
import com.icoolkj.ms.common.cache.constant.CacheNames;
import com.icoolkj.ms.common.core.enums.UserTypeEnum;
import com.icoolkj.ms.common.core.response.ServerResponseEntity;
import com.icoolkj.ms.common.core.utils.PrincipalUtil;
import com.icoolkj.ms.common.security.bo.TokenInfoBO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author icoolkj
 * @version 1.0
 * @description token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
 * @createDate 2025/02/15 11:38
 */
@Component
@RefreshScope
public class TokenManager {

    private static final Logger logger = LoggerFactory.getLogger(TokenManager.class);

    private static final int MAX_TOKENS_PER_USER = 5;

    private static final int DEFAULT_EXPIRES_IN = 3600;

    private final RedisTemplate<Object, Object> redisTemplate;

    private final StringRedisTemplate stringRedisTemplate;

    public TokenManager(RedisTemplate<Object, Object> redisTemplate,
                        StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 存储并返回Token信息
     * @param userInfoInTokenBO 用户信息
     * @return Token信息
     */
    public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInTokenBO) {
        TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInTokenBO);
        return buildTokenInfoVO(tokenInfoBO);
    }

    private TokenInfoVO buildTokenInfoVO(TokenInfoBO tokenInfoBO) {
        TokenInfoVO tokenInfoVO = new TokenInfoVO();
        tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
        tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
        tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn());
        return tokenInfoVO;
    }

    /**
     * 将用户的部分信息存储在token中，并返回token信息
     * @param userInfoInTokenBO 用户信息
     * @return token信息
     */
    public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInTokenBO) {
        String accessToken = generateToken();
        String refreshToken = generateToken();
        TokenInfoBO tokenInfoBO = new TokenInfoBO();

        tokenInfoBO.setUserInfoInTokenBO(userInfoInTokenBO);
        tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInTokenBO.getUserType()));

        String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(userInfoInTokenBO));
        String accessKeyStr = getAccessKey(accessToken);
        String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken);

        // 使用 Hash 结构存储用户的所有 Token
        Map<String, String> tokenMap = getExistingTokens(uidToAccessKeyStr);
        tokenMap.put(accessToken, refreshToken);

        cleanUpTokensIfNeeded(tokenMap);

        storeTokensInRedis(uidToAccessKeyStr, refreshToAccessKeyStr, accessKeyStr, userInfoInTokenBO, tokenMap, accessToken);

        // 返回给前端是加密的token
        tokenInfoBO.setAccessToken(encryptToken(accessToken, userInfoInTokenBO.getUserType()));
        tokenInfoBO.setRefreshToken(encryptToken(refreshToken, userInfoInTokenBO.getUserType()));

        return tokenInfoBO;
    }

    /**
     * 将Token信息存储到Redis
     */
    private void storeTokensInRedis(String uidToAccessKeyStr, String refreshToAccessKeyStr, String accessKeyStr,
                                    UserInfoInTokenBO userInfoInTokenBO, Map<String, String> tokenMap, String accessToken) {
        redisTemplate.executePipelined((RedisCallback<?>) connection -> {
            long expiresIn = getExpiresIn(userInfoInTokenBO.getUserType());

            // 1. 将多个 accessToken 添加到集合中
            redisTemplate.opsForHash().putAll(uidToAccessKeyStr, tokenMap);

            // 2. 设置集合的过期时间
            redisTemplate.expire(uidToAccessKeyStr, expiresIn, TimeUnit.SECONDS);

            // 3. 设置 refreshToken 到 accessToken 的映射，并设置过期时间
            redisTemplate.opsForValue().set(refreshToAccessKeyStr, accessToken, expiresIn, TimeUnit.SECONDS);

            // 4. 设置 accessToken 到用户信息的映射，并设置过期时间
            redisTemplate.opsForValue().set(accessKeyStr, userInfoInTokenBO, expiresIn, TimeUnit.SECONDS);

            return null;
        });
    }

    /**
     * 如果Token超过最大数量，删除最早的Token
     */
    private void cleanUpTokensIfNeeded(Map<String, String> tokenMap) {
        if (tokenMap.size() > MAX_TOKENS_PER_USER) {
            tokenMap.entrySet().iterator().next();
            tokenMap.remove(tokenMap.keySet().iterator().next());
        }
    }

    private Map<String, String> getExistingTokens(String uidToAccessKeyStr) {
        Map<Object, Object> existingTokens = redisTemplate.opsForHash().entries(uidToAccessKeyStr);
        Map<String, String> tokenMap = new HashMap<>();
        for (Map.Entry<Object, Object> entry : existingTokens.entrySet()) {
            tokenMap.put((String) entry.getKey(), (String) entry.getValue());
        }
        return tokenMap;
    }

    /**
     * 获取当前用户的有效期 （单位：秒）
     */
    private int getExpiresIn(String userType) {
        if (ObjectUtil.equal(UserTypeEnum.SYSTEM_USER.getCode(), userType)) {   // 系统管理员2小时
            return DEFAULT_EXPIRES_IN * 2;
        }
        return DEFAULT_EXPIRES_IN;
    }

    private static String getApprovalKey(UserInfoInTokenBO userInfoInTokenBO) {
        return getApprovalKey(userInfoInTokenBO.getUserType(), userInfoInTokenBO.getUid());
    }

    private static String getApprovalKey(String appId, Long uid) {
        return uid == null?  appId : appId + StrUtil.COLON + uid;
    }

    public String getUidToAccessKey(String approvalKey) {
        return CacheNames.UID_TO_ACCESS + approvalKey;
    }

    public String getAccessKey(String accessToken) {
        return CacheNames.ACCESS + accessToken;
    }

    public String getRefreshToAccessKey(String refreshToken) {
        return CacheNames.REFRESH_TO_ACCESS + refreshToken;
    }

    private String encryptToken(String accessToken, String userType) {
        return Base64.encode(accessToken + System.currentTimeMillis() + userType);
    }

    private String generateToken() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 刷新token，并返回新的token
     * @param refreshToken
     * @return
     */
    public ServerResponseEntity<TokenInfoBO> refreshToken(String refreshToken) {
        if (StrUtil.isBlank(refreshToken)) {
            return ServerResponseEntity.fail("refreshToken is blank");
        }

        ServerResponseEntity<String> decryptTokenEntity = decryptToken(refreshToken);
        if (!decryptTokenEntity.isSuccess()) {
            return ServerResponseEntity.transform(decryptTokenEntity);
        }

        String realRefreshToken = decryptTokenEntity.getData();
        String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken));
        if (StrUtil.isBlank(accessToken)) {
            return ServerResponseEntity.fail("refreshToken 已过期");
        }

        ServerResponseEntity<UserInfoInTokenBO> userInfoByAccessTokenEntity = getUserInfoByAccessToken(accessToken, false);
        if (!userInfoByAccessTokenEntity.isSuccess()) {
            return ServerResponseEntity.fail("refreshToken 已过期");
        }

        UserInfoInTokenBO userInfoInTokenBO = userInfoByAccessTokenEntity.getData();
        // 删除旧的refresh_token
        stringRedisTemplate.delete(getRefreshToAccessKey(realRefreshToken));
        // 删除旧的access_token
        stringRedisTemplate.delete(getAccessKey(accessToken));
        // 保存一份新的token
        TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInTokenBO);

        return ServerResponseEntity.success(tokenInfoBO);
    }

    /**
     * 根据accessToken 获取用户信息
     * @param accessToken accessToken
     * @param needDecrypt 是否需要解密
     * @return 用户信息
     */
    public ServerResponseEntity<UserInfoInTokenBO> getUserInfoByAccessToken(String accessToken, boolean needDecrypt) {
        if (StrUtil.isBlank(accessToken)) {
            return ServerResponseEntity.fail("accessToken is blank");
        }

        String realAccessToken = needDecrypt ? decryptToken(accessToken).getData() : accessToken;
        UserInfoInTokenBO userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue().get(getAccessKey(realAccessToken));

        if (userInfoInTokenBO == null) {
            return ServerResponseEntity.fail("accessToken 已过期");
        }
        return ServerResponseEntity.success(userInfoInTokenBO);
    }

    private ServerResponseEntity<String> decryptToken(String data) {
        try {
            String decryptStr = Base64.decodeStr(data);
            String token = decryptStr.substring(0,32); // Token
            long createTokenTime = Long.parseLong(decryptStr.substring(32,45));  // Token创建时间
            String userType = decryptStr.substring(45); // 用户类型

            int expiresIn = getExpiresIn(userType); // token的过期时间
            if (System.currentTimeMillis() - createTokenTime >  expiresIn * 1000L) {
                return ServerResponseEntity.fail("token 已过期");
            }

            // 防止解密后的token是脚本，从而对redis进行攻击，uuid只能是数字和小写字母
            if (!PrincipalUtil.isSimpleChar(token)) {
                return ServerResponseEntity.fail("token 格式有误");
            }
            return ServerResponseEntity.success(token);
        }
        catch (Exception e) {
            logger.error("Token 解密失败", e);
            return ServerResponseEntity.fail("token 格式有误");
        }
    }

    /**
     * 删除全部的token
     */
    public void deleteAllToken(String appId, Long uid) {
        String uidToAccessKeyStr = getUidToAccessKey(getApprovalKey(appId, uid));
        Map<String, String> tokenMap = getExistingTokens(uidToAccessKeyStr);

        // 如果 tokenMap 不为空，遍历删除 accessToken 和 refreshToken 关联的数据
        if (tokenMap != null && !tokenMap.isEmpty()) {
            for (Map.Entry<String, String> entry : tokenMap.entrySet()) {
                String accessToken = entry.getKey();
                String refreshToken = entry.getValue();
                redisTemplate.delete(getRefreshToAccessKey(refreshToken));
                redisTemplate.delete(getAccessKey(accessToken));
            }
        }

        // 删除 uid 对应的 accessKey
        redisTemplate.delete(uidToAccessKeyStr);
    }

}
